// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: jhkim

#pragma once

#include <memory>
#include <vector>
#include <map>
#include <string>
#include <utility>
#include <unordered_map>
#include <unordered_set>

#include "presto/quant/math/moving_average.h"
#include "presto/quant/math/sampled_moving_average.h"

#include "coin/proto/coin_executor.pb.h"
#include "coin/proto/coin_order_enums.pb.h"
#include "coin/proto/coin_telemetry.pb.h"

#include "coin2/exchange/base/log/strategy_logger.h"
#include "coin2/exchange/base/symbology/product_encyclopedia.h"

#include "coin2/strategy/linear_model/logic.h"
#include "coin2/strategy/linear_model/util.h"
#include "coin2/strategy/linear_model/mm_util.h"
#include "coin2/strategy/linear_model/product_strategy.h"
#include "coin2/strategy/order_executor/executor.h"


namespace coin2::strategy::linear_model {

using ::coin2::exchange::base::symbology::ProductEncyclopedia;
using ::coin2::exchange::base::order::OrderUpdate;
using ::coin2::strategy::order_executor::OrderExecutorSystem;

inline double GetMinPosQuote(
    const LmFocusConfig& focus_config,
    std::optional<double> max_pos_multiplier_override = std::nullopt) {
  if (!focus_config.has_max_abspos_quote() || !focus_config.has_max_pos_multiplier()) {
    CHECK(focus_config.has_min_pos_quote()) << "min_pos_quote has no value";
    return focus_config.min_pos_quote();
  } else {
    if (!max_pos_multiplier_override.has_value()) {
      max_pos_multiplier_override = focus_config.max_pos_multiplier();
    }
    double ret = focus_config.max_abspos_quote() * (-*max_pos_multiplier_override);
    if (focus_config.has_min_pos_quote()) ret = std::max(focus_config.min_pos_quote(), ret);
    return ret;
  }
}

inline double GetMaxPosQuote(
    const LmFocusConfig& focus_config,
    std::optional<double> max_pos_multiplier_override = std::nullopt) {
  if (!focus_config.has_max_abspos_quote() || !focus_config.has_max_pos_multiplier()) {
    CHECK(focus_config.has_max_pos_quote()) << "max_pos_quote has no value";
    return focus_config.max_pos_quote();
  } else {
    if (!max_pos_multiplier_override.has_value()) {
      max_pos_multiplier_override = focus_config.max_pos_multiplier();
    }
    double ret = focus_config.max_abspos_quote() * *max_pos_multiplier_override;
    if (focus_config.has_max_pos_quote()) ret = std::min(focus_config.max_pos_quote(), ret);
    return ret;
  }
}

inline double GetReserve(
    const LmFocusConfig& focus_config,
    const Symbol* symbol) {
  presto::Mep mep;
  double reserve = 0.0;
  for (const auto& sr: focus_config.symbols_reserve()) {
    mep.market_type = sr.market_type();
    mep.exchange_name = sr.exchange_name();
    mep.product_name = sr.product_name();
    if (symbol->symbol_string() == mep.GetSymbolString()) {
      reserve = sr.reserve();
      break;
    }
  }
  return reserve;
}

struct AggregatedSizer {
  explicit AggregatedSizer(const LmFocusConfig& focus_config)
      : focus_config(focus_config) {}

  void Init(
       int64_t curr_time,
       const MarketExchangeApi& mea,
       const std::vector<const Symbol*>& symbols) {
    for (const auto& prod : focus_config.symbols()) {
      auto product = CreateProductFromNormString(
          mea,
          prod,
          curr_time,
          false);
      if (product) {
        auto mep = presto::Mep{
            MarketType_Name(product->market()),
            ExchangeType_Name(product->exchange()),
            product->relative_norm()
            };
        for (const Symbol* symbol : symbols) {
          if (symbol->symbol_string() == mep.GetSymbolString()) {
            LOG(INFO) << "Trade: " << symbol->symbol_string();
            symbol_index[symbol] = num_focus_index;
            num_focus_index++;
          }
        }
      }
    }
    tpos_ma_for_uhedge.resize(num_focus_index);
    mixed_quote_for_nhedge.resize(num_focus_index);
    vtpos_scaled_for_capa.resize(num_focus_index);
    vtpos_scaled_online.resize(num_focus_index);
    pr_symbols.resize(num_focus_index);

    for (const auto& prod_clear : focus_config.symbols_clear()) {
      auto product = CreateProductFromNormString(
          mea,
          prod_clear,
          curr_time,
          false);
      if (product) {
        auto mep = presto::Mep{
            MarketType_Name(product->market()),
            ExchangeType_Name(product->exchange()),
            product->relative_norm()
            };
        for (const Symbol* symbol : symbols) {
          if (symbol->symbol_string() == mep.GetSymbolString()) {
            LOG(INFO) << "Clear: " << symbol->symbol_string();
            symbols_clear.insert(symbol);
          }
        }
      }
    }
  }

  void UpdatePosition(const Symbol* symbol, double curr_pos_quote) {
    double prev_pos_quote = pos_quote[symbol];
    pos_quote[symbol] = curr_pos_quote;
    pos_quote_sum += curr_pos_quote - prev_pos_quote;
    pos_quote_abssum += (std::abs(curr_pos_quote) - std::abs(prev_pos_quote));
  }

  double DesiredBufferPosQuote() const {
    return (-pos_quote_sum * 0.5);
  }

  std::pair<double, double> GetRemainingBuySellQuote() {
    double remaining_buy = GetMaxPosQuote(focus_config) - pos_quote_sum;
    double remaining_sell = pos_quote_sum - GetMinPosQuote(focus_config);
    return std::make_pair(remaining_buy, remaining_sell);
  }

  std::pair<double, double> GetRemainingAbsBuySellQuote(const Symbol* symbol) {
    double max_quote = focus_config.max_abspos_quote();
    max_quote = std::max(max_quote, pos_quote_abssum);
    double curr_pos_quote = pos_quote[symbol];
    double remaining_buy = 0, remaining_sell = 0;
    if (curr_pos_quote > 0) {
      remaining_buy = max_quote - pos_quote_abssum;
      remaining_sell = std::max(curr_pos_quote,
          max_quote - pos_quote_abssum + 2 * curr_pos_quote);
    } else {
      remaining_buy = std::max(-curr_pos_quote,
          max_quote - pos_quote_abssum - 2 * curr_pos_quote);
      remaining_sell = max_quote - pos_quote_abssum;
    }
    return std::make_pair(remaining_buy, remaining_sell);
  }

  double GetAggregatedNormalizedPos(bool use_reserve) const {
    if (use_reserve) {
      double max_pos_quote = GetMaxPosQuote(focus_config);
      double min_pos_quote = GetMinPosQuote(focus_config);
      double mean_pos_quote = 0.5 * (max_pos_quote + min_pos_quote);

      return (pos_quote_sum - mean_pos_quote) / (
          GetMaxPosQuote(focus_config) - GetMinPosQuote(focus_config));
    } else {
      return pos_quote_sum / (
          GetMaxPosQuote(focus_config) - GetMinPosQuote(focus_config));
    }
  }

  LmFocusConfig focus_config;
  double pos_quote_sum = 0, pos_quote_abssum = 0;
  std::unordered_map<const Symbol*, double> pos_quote;
  std::unordered_set<const Symbol*> symbols_clear;
  std::unordered_map<const Symbol*, int> symbol_index;
  int num_focus_index = 0;
  presto::math::MeanOnline tpos_ma_for_uhedge;
  presto::math::MeanOnline mixed_quote_for_nhedge;
  presto::math::MeanOnline vtpos_scaled_for_capa;
  presto::math::MeanOnline vtpos_scaled_online;
  presto::math::MeanOnline pr_symbols;
};


struct LmSubStrategy {
  LmSubStrategy(
      const MarketExchangeApi& mea,
      const IProduct* product,
      const Symbol* symbol,
      const LmProductConfig& lm_product_config,
      int64_t strat_init_time,
      ProductEncyclopedia* product_cache)
      : mea(mea),
        symbol(symbol),
        focus_config(lm_product_config.focus_config()),
        bom(strat_init_time),
        lholder_(mea, *product, symbol, product_cache),
        virtual_target_norpos_interval(lm_product_config.focus_config().prediction_window_sec() * 1e9),
        vtpos_doubleinterval(
          lm_product_config.focus_config().prediction_window_sec() * 1e9,
          lm_product_config.focus_config().pred_ub_sec() * 1e9),
        vtpos_for_ub(lm_product_config.focus_config().pred_ub_sec() * 1e9) {
    constexpr int64_t NS = 1'000'000'000LL;
    price_window_.ResizeWindow(3600LL * 24LL * NS);
    trading_pnls_.ResizeWindow(focus_config.loss_penalty_window_in_sec() * NS);
    holding_pnls_.ResizeWindow(focus_config.loss_penalty_window_in_sec() * NS);
    dmove_interval.ResizeWindow(lm_product_config.focus_config().pred_ub_sec() * NS);
    bp_volatility_window.ResizeWindow(lm_product_config.focus_config().bp_volatility_window_sec() * NS);
    if (focus_config.ver() == 0 && focus_config.symbolwise_adjust_exponent() != 0.0) {
      market_turnover_.ResizeWindow(focus_config.pr_window_in_sec() * NS, 1LL * NS);
      sell_turnover_.ResizeWindow(focus_config.pr_window_in_sec() * NS);
      buy_turnover_.ResizeWindow(focus_config.pr_window_in_sec() * NS);
    }
    if ((focus_config.ver() == 2) || (focus_config.ver() == 3)) {
      liquidity_.ResizeWindow(3600LL * 24LL * NS, 60LL * NS);
      sell_filled_.ResizeWindow(focus_config.fill_cooldown_window() * NS);
      buy_filled_.ResizeWindow(focus_config.fill_cooldown_window() * NS);
      market_turnover_.ResizeWindow(focus_config.pr_window_in_sec() * NS, 1LL * NS);
      sell_turnover_.ResizeWindow(focus_config.pr_window_in_sec() * NS);
      buy_turnover_.ResizeWindow(focus_config.pr_window_in_sec() * NS);
      ask_bbo_.ResizeWindow(focus_config.book_window_in_sec() * NS);
      bid_bbo_.ResizeWindow(focus_config.book_window_in_sec() * NS);
      if (StringStartsWith(symbol->product_name(), std::string("BTC-")) ||
          StringStartsWith(symbol->product_name(), std::string("ETH-"))) {
        is_buffer_symbol = true;
      }
    }
    reserve = GetReserve(focus_config, symbol);

    if (lm_product_config.has_leverage_rate()) {
      leverage_rate = lm_product_config.leverage_rate();
    } else {
      leverage_rate = 5;
    }
  }

  void WarmupVariableFromStratLog(
      coin2::exchange::base::strategy_util::StrategyLogger* strat_logger,
      int64_t curr_time) {
    std::string log_root = strat_logger->GetLogRoot();
    std::string machine = strat_logger->GetMachine();
    coin::proto::StrategyRequestProto strat_request = strat_logger->GetStrategyRequestProto();
    const size_t BUF_SIZE = 1000 * 1024;
    coin2::exchange::base::strategy_util::StrategyLogReader reader(
        strat_request, log_root, machine,
        curr_time - 600 * Time::kNanosecondsPerSecond, curr_time, BUF_SIZE);
    coin::proto::StrategyLog strat_log_proto;
    std::string prod_name = lholder_.target_product.key_string();

    while (reader.Read(&strat_log_proto)) {
      if (strat_log_proto.type() != coin::proto::StrategyLog::STRAT_STORED_VALUE
          || strat_log_proto.strat_stored_value().product_name() != prod_name) {
        continue;
      }
      // Load Part
      if (strat_log_proto.strat_stored_value().value_type() == 3) {
        virtual_target_norpos_ma =
          strat_log_proto.strat_stored_value().focus_warmup_var().virtual_target_norpos_ma();
      }
    }
    LOG(INFO) << "** " << prod_name << " virtual_target_norpos_ma: " << virtual_target_norpos_ma;
    this->strat_logger = strat_logger;
  }

  void LogggingWarmupVariableFocus3(int64_t timestamp) {
    if (!strat_logger || last_warm_logged_ts_ + 60'000'000'000LL >= timestamp) {
      return;
    }
    coin::proto::StrategyStoredValue stored_value;
    stored_value.set_product_name(lholder_.target_product.key_string());
    stored_value.set_value_type(3);
    stored_value.mutable_focus_warmup_var()->set_virtual_target_norpos_ma(virtual_target_norpos_ma);
    strat_logger->WriteStratStoredValue(stored_value, true, timestamp);
    last_warm_logged_ts_ = timestamp;
  }

  void UpdateLiquidity(int64_t fetched_time, double pq) {
    liquidity_.Update(fetched_time, pq);
  }

  void UpdatePrice(int64_t time, double price) {
    price_window_.Update(time, price);
  }

  void UpdateSignal(
      int64_t timestamp,
      int64_t* first_feed_time,
      double midret_pred,
      std::optional<double> midtomed_pred,
      OrderExecutorSystem* oe,
      PredictionInterval* pred_interval,
      AggregatedSizer* asizer);

  // for focus2 (ver == 2)
  void UpdateSignal2(
      int64_t timestamp,
      int64_t* first_feed_time,
      double midret_pred,
      std::optional<double> midret_pred_sell,
      std::optional<double> midtomed_pred,
      std::optional<double> midtomax_pred,
      std::optional<double> midtomin_pred,
      std::optional<double> residual_pred,
      OrderExecutorSystem* oe,
      PredictionInterval* pred_interval,
      AggregatedSizer* asizer,
      std::vector<double> signals);

  // for focus3 (ver == 3)
  void UpdateSignal3(
      int64_t timestamp,
      int64_t* first_feed_time,
      double midret_pred,
      std::optional<double> midret_pred_sell,
      std::optional<double> midtomed_pred,
      std::optional<double> midtomax_pred,
      std::optional<double> midtomin_pred,
      std::optional<double> residual_pred,
      OrderExecutorSystem* oe,
      PredictionInterval* pred_interval,
      AggregatedSizer* asizer,
      double curr_shared_stamp);

  void OnBufferSignal(
      int64_t timestamp,
      int64_t* first_feed_time,
      double midret_pred,
      double signal_sigma,
      OrderExecutorSystem* oe,
      PredictionInterval* pred_interval,
      AggregatedSizer* asizer);

  void ClearPositionWoSignal(
      int64_t timestamp,
      OrderExecutorSystem* oe,
      AggregatedSizer* asizer);

  void onAccountInfo(const OrderUpdate& upd, OrderExecutorSystem* oe) {
    if (!pholder) {
      SPDLOG_INFO_EVERY_NS(5e9, upd.timestamp(), "Product Holder not ready, {}", symbol->symbol_string());
      return;
    }
    if (!oe->HasProduct(*pholder)) {
      SPDLOG_INFO("Does not have product: {}", pholder->product().absolute_norm());
      return;
    }
    oe->onAccountInfo(*pholder, upd);
  }
  void onAccountOrder(const OrderUpdate& upd, OrderExecutorSystem* oe) {
    if (!pholder) {
      SPDLOG_INFO("Product Holder not ready.");
      return;
    }
    oe->onAccountOrder(*pholder, upd);
    if (upd.product_order_info().event().type() == coin::proto::OrderEvent::ORDER_FILLED) {
      UpdateFill(
            oe,
            upd.product_order_info().event().event_time(),
            upd.product_order_info().event().trade_side(),
            upd.product_order_info().event().fill_price(),
            upd.product_order_info().event().fill_qty());
    }
  }

  void UpdateBbo(const Bbo& bbo) {
    this->bbo = bbo;
    price_window_.Update(bbo.fetched_time, 0.5 * (bbo.askp + bbo.bidp));
  }

  void ManageExecutorConfig(int64_t timestamp) {
    CHECK(pholder);
    CHECK(!price_window_.empty());

    double min_quote = GetMinPosQuote(focus_config) * focus_config.max_concentration();
    double max_quote = GetMaxPosQuote(focus_config) * focus_config.max_concentration();

    double max_pos_config = util::GetQtyFromQuoteSafe(
        *pholder,
        price_window_.last_element(),
        price_window_.first_element(),
        max_quote);
    double min_pos_config = util::GetQtyFromQuoteSafe(
        *pholder,
        price_window_.last_element(),
        price_window_.first_element(),
        min_quote);
    double lot_size_config = (max_pos_config - min_pos_config) / focus_config.stack();
    lholder_.ManageExecutorConfig(lot_size_config, min_pos_config, max_pos_config, timestamp);
  }

  void ManageExecutorConfig2(int64_t timestamp, OrderExecutorSystem* oe) {
    CHECK(pholder);
    CHECK(!price_window_.empty());
    lholder_.ManageExecutorConfig(
      oe->GetConfig(*pholder).qty().max_lot_size(),
      oe->GetConfig(*pholder).qty().min_position(),
      oe->GetConfig(*pholder).qty().max_position(),
      timestamp);
  }

  void UpdateProduct(
      ProductEncyclopedia* product_cache,
      const IProduct& product,
      int64_t timestamp_override) {
    CHECK_EQ(mea.exchange, product.exchange());
    if (!pholder) pholder = &(product_cache->holder(product));
  }

  void UpdateFill(
      OrderExecutorSystem* oe,
      int64_t timestamp,
      coin::proto::TradeSide trade_side,
      double fill_price,
      double fill_qty) {
    if (!bbo || !oe->HasPosition(*pholder)) return;

    double lot_size = oe->GetConfig(*pholder).qty().max_lot_size();
    double curr_pos = GetCurrPos(oe);
    lot_size *= focus_config.maker_qty_ratio();
    switch (trade_side) {
      case coin::proto::TRADE_SELL_SIDE:
        ask_fill_qty += fill_qty;
        sell_filled_.Update(timestamp, fill_qty);
        if (ask_fill_qty >= lot_size) {
          bom.prev_ask_fill_time = timestamp;
          ask_fill_qty = 0;
        }
        trading_pnls_.Update(timestamp, fill_price * fill_qty);
        holding_pnls_.Update(timestamp, curr_pos * 0.5 * (bbo->askp + bbo->bidp));
        sell_turnover_.Update(timestamp, fill_price * fill_qty);
        break;
      case coin::proto::TRADE_BUY_SIDE:
        bid_fill_qty += fill_qty;
        buy_filled_.Update(timestamp, fill_qty);
        if (bid_fill_qty >= lot_size) {
          bom.prev_bid_fill_time = timestamp;
          bid_fill_qty = 0;
        }
        trading_pnls_.Update(timestamp, -fill_price * fill_qty);
        holding_pnls_.Update(timestamp, curr_pos * 0.5 * (bbo->askp + bbo->bidp));
        buy_turnover_.Update(timestamp, fill_price * fill_qty);
        break;
      default:
        break;
    }
  }

  void UpdateMidp(
        StrategyReporter2* strat_reporter,
        AggregatedSizer* asizer) {
    if (!bbo || !pholder) return;
    bool use_reserve = focus_config.use_reserve();
    if (asizer->symbols_clear.count(symbol) > 0) {
      use_reserve = false;
    }

    strat_reporter->UpdateMidp(pholder->product(), 0.5 * (bbo->askp + bbo->bidp));
    lholder_.UpdateMidp(
        strat_reporter,
        &pholder->product(),
        use_reserve,
        true);
  }

  void UpdateTrade(double price, double qty, int64_t fetched_time) {
    double pq = price * qty;
    market_turnover_.Update(fetched_time, pq);
    UpdateLiquidity(fetched_time, pq);
  }

  void UpdateBboPq(double ask0_pq, double bid0_pq, int64_t fetched_time) {
    ask_bbo_.Update(fetched_time, ask0_pq);
    bid_bbo_.Update(fetched_time, bid0_pq);
  }

  double GetCurrPos(OrderExecutorSystem* oe) {
    return oe->GetCurrentPosition(*pholder) - reserve;
  }

  double GetCurrStack(OrderExecutorSystem* oe, double lot_size) {
    return oe->GetCurrentStackAbsolute(*pholder) - reserve / lot_size;
  }

  double GetMidpMove() {
    double midp = 0.5 * (bbo->askp + bbo->bidp);
    double midp_move = 0.0;
    if (last_midp != 0.0) {
      midp_move = (midp - last_midp) / (0.5 * (midp + last_midp));
    }
    last_midp = midp;
    return midp_move;
  }

  void UpdateCurrSubStamp(double dt) {
    curr_sub_stamp += dt;
  }

  presto::math::TimeWindowMovingAverage<double> price_window_;
  presto::math::TimeWindowMovingAverage<double> ask_bbo_, bid_bbo_;
  presto::math::TimeWindowMovingAverage<double> buy_filled_, sell_filled_;
  presto::math::TimeWindowMovingAverage<double> buy_turnover_, sell_turnover_;
  presto::math::SampledMovingAverage<double> liquidity_, market_turnover_;
  MarketExchangeApi mea;
  const Symbol* symbol;
  LmFocusConfig focus_config;
  // used for huobi, kucoin
  double leverage_rate = 0;
  std::optional<Bbo> bbo;
  const coin2::exchange::base::symbology::IProductHolder* pholder = nullptr;
  bool cleared = false;
  BlockOrderMeasures bom;
  double bid_fill_qty = 0;
  double ask_fill_qty = 0;
  presto::math::MovingAverageWindow<double> trading_pnls_;
  presto::math::MovingAverageWindow<double> holding_pnls_;
  bool is_buffer_symbol = false;
  int64_t last_logged_ts_ = 0L;
  int64_t last_debug_logged_ts_ = 0L;
  int64_t last_warm_logged_ts_ = 0L;
  LogicHolder lholder_;
  double reserve = 0.0;
  int64_t last_pred_time = 0L;
  double last_pred_stamp = 0.0;
  double last_midp = 0.0;
  double curr_sub_stamp = 0.0;
  double virtual_target_norpos_ma = 0.0;
  double prev_vtpos_for_ub_sum = 0.0;
  presto::math::MovingAverageWindow<double> dmove_interval;
  presto::math::MovingAverageWindow<double> bp_volatility_window;
  PositionInterval virtual_target_norpos_interval;
  PositionDoubleInterval vtpos_doubleinterval;
  PositionInterval vtpos_for_ub;
  coin2::exchange::base::strategy_util::StrategyLogger* strat_logger;
};


// represents 1 symbol
class LmFocusLogic : public LinearModelLogic {
 public:
  explicit LmFocusLogic(
      const MarketExchangeApi& mea,
      const IProduct& target_product,
      const Symbol* target_symbol,
      const LmProductConfig& lm_product_config,
      ProductEncyclopedia* product_cache,
      const ::presto::SymbolManager* symbol_manager)
      : LinearModelLogic(mea, target_product, target_symbol, product_cache),
        enable_telemetry_(lm_product_config.enable_telemetry()),
        mea_(mea),
        lm_product_config_(lm_product_config),
        sim_account_id_(lm_product_config.sim_account_id()),
        product_cache_(product_cache),
        prediction_window_sec(lm_product_config.focus_config().prediction_window_sec()),
        aggregated_pred_interval_(lm_product_config.focus_config().prediction_window_sec() * 1e9),
        use_prediction_interval_per_symbol(lm_product_config.focus_config().prediction_interval_per_symbol()),
        asizer_(lm_product_config.focus_config()),
        symbol_manager_(symbol_manager) {
  }

  void InitImpl(
      ::coin2::app::Driver* driver,
      coin2::exchange::base::strategy_util::StrategyLogger* strat_logger) override {
    strat_logger_ = strat_logger;
    auto curr_time = timestamp_override_;
    symbol_manager_->GetAllSymbols(&symbols_);
    asizer_.Init(curr_time, mea_, symbols_);
    coin2::exchange::symbology::ProductRequest req;
    req.set_mea(mea_.String());
    for (auto& [name, oe] : driver->strategy()->order_executors(sim_account_id_)) {
      (void)name;
      if (oe->mea() == mea_) {
        oe_ = oe.get();
        if (enable_telemetry_) {
          LOG(INFO) << "Set Latency Recorder";
          oe_->order_system()->gateway()->set_latency_recorder(
              strat_logger->mutable_latency_recorder());
        }
        for (auto& [prod, prod_conf] : oe->config().products()) {
          req.set_norm(prod);
          for (auto&& product:
              ResolveProductRequestFromPotentiallyWildcard(req, curr_time, false)) {
            products_.push_back(std::move(product));
            if (sim_account_id_ == prod_conf.sim_account_id()) {
              const auto& product = products_.back();
              const Symbol* symbol = product_strategy_->ResolveSymbol(mea_, product->relative_norm());
              if (!symbol) {
                continue;
              }
              if (strats_.count(symbol) == 0) {
                strats_[symbol].reset(new LmSubStrategy(mea_, product.get(), symbol, lm_product_config_, curr_time, product_cache_));
              }
              if (pred_intervals_.count(symbol) == 0) {
                pred_intervals_[symbol].reset(new PredictionInterval(prediction_window_sec * 1e9));
              }
              CHECK_EQ(strats_.count(symbol), 1) << symbol->symbol_string();
              prodstrats_[GetProductKey(product.get())] = strats_.at(symbol).get();
              configs_.resize(configs_.size() + 1);
              auto& config = configs_.back();
              config.Clear();
              config.set_symbol(product->absolute_norm());
              config.set_lot_size(prod_conf.qty().max_lot_size());
              config.set_min_pos(prod_conf.qty().min_position());
              config.set_max_pos(prod_conf.qty().max_position());
            }
          }
        }
      }
    }
    for (auto& [name, oe] : driver->strategy()->order_executors(sim_account_id_)) {
      (void)name;
      if (oe->mea() == mea_) {
        lholder_.InitExecutor(oe.get(), strat_logger, enable_telemetry_, curr_time, true);
        for (auto&& [symbol, substrat] : strats_) {
          CHECK(lholder_.acc_req.has_value());
          substrat->lholder_.InitAccReq(lholder_.acc_req.value());
          substrat->lholder_.InitExecutor(oe.get(), strat_logger, false, curr_time, true);
        }
      }
    }
  }

  void OnFill(
      int64_t timestamp,
      coin::proto::TradeSide trade_side,
      double fill_price,
      double fill_qty) final {
  }

  void onAccountInfo(const OrderUpdate& upd) final {
    if (!oe_) return;
    if (!upd.is_account_info()) return;
    if (upd.sim_account_id && sim_account_id_ != *upd.sim_account_id) return;
    for (auto&& [symbol, substrat] : strats_) {
      (void)symbol;
      substrat->onAccountInfo(upd, oe_);
    }
  }

  void onAccountOrder(const OrderUpdate& upd) final {
    if (!oe_) return;
    if (upd.sim_account_id && sim_account_id_ != *upd.sim_account_id) return;
    auto pkey = GetProductKey(&upd.product());
    auto it = prodstrats_.find(pkey);
    if (it != prodstrats_.end() && it->second) {
      it->second->onAccountOrder(upd, oe_);
    }
  }
  void UpdateMidp(
      StrategyReporter2* strat_reporter,
      const IProduct* target_product_unused) final {
    for (auto&& [symbol, substrat] : strats_) {
      (void)symbol;
      substrat->UpdateMidp(strat_reporter, &asizer_);
    }
  }

  void onFeed(
      const Symbol* symbol,
      const FeedUpdate& upd,
      const ::fastfeature::FeedMessage& feed_msg,
      LinearModel* linear_model,
      int64_t* lm_feed_id,
      const int64_t* global_feed_id,
      LinearModelDelegateProto* delegate,
      bool simulation) override;

 private:
  std::vector<std::unique_ptr<IProduct>> products_;
  const bool enable_telemetry_;
  const MarketExchangeApi mea_;
  LmProductConfig lm_product_config_;
  int64_t sim_account_id_;
  int64_t first_feed_time_ = 0L;
  ProductEncyclopedia* product_cache_ = nullptr;
  OrderExecutorSystem* oe_ = nullptr;
  std::unordered_map<const Symbol*, std::unique_ptr<LmSubStrategy>> strats_;
  std::unordered_map<std::string, LmSubStrategy*> prodstrats_;
  std::vector<coin::proto::coin_executor::AggressiveExecutorConfig> configs_;
  int32_t prediction_window_sec;
  PredictionInterval aggregated_pred_interval_;
  std::unordered_map<const Symbol*, std::unique_ptr<PredictionInterval>> pred_intervals_;
  bool use_prediction_interval_per_symbol;
  AggregatedSizer asizer_;
  const ::presto::SymbolManager* symbol_manager_;
  std::vector<const Symbol*> symbols_;
  int prem_neut_feat_idx_ = -1;

  // focus does not use logic holder.
  coin2::exchange::base::strategy_util::StrategyLogger* strat_logger_;
};

}  // namespace coin2::strategy::linear_model
